// Copyright (c) 2010 SuccessFactors, Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
//     * Redistributions of source code must retain the above
//       copyright notice, this list of conditions and the following
//       disclaimer.
//
//     * Redistributions in binary form must reproduce the above
//       copyright notice, this list of conditions and the following
//       disclaimer in the documentation and/or other materials
//       provided with the distribution.
//
//     * Neither the name of the SuccessFactors, Inc. nor the names of
//       its contributors may be used to endorse or promote products
//       derived from this software without specific prior written
//       permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
// COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
// OF THE POSSIBILITY OF SUCH DAMAGE.

package org.owasp.jxt.compiler;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import org.eclipse.jdt.core.compiler.IProblem;
import org.eclipse.jdt.internal.compiler.ClassFile;
import org.eclipse.jdt.internal.compiler.CompilationResult;
import org.eclipse.jdt.internal.compiler.Compiler;
import org.eclipse.jdt.internal.compiler.DefaultErrorHandlingPolicies;
import org.eclipse.jdt.internal.compiler.ICompilerRequestor;
import org.eclipse.jdt.internal.compiler.IErrorHandlingPolicy;
import org.eclipse.jdt.internal.compiler.IProblemFactory;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileReader;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFormatException;
import org.eclipse.jdt.internal.compiler.env.ICompilationUnit;
import org.eclipse.jdt.internal.compiler.env.INameEnvironment;
import org.eclipse.jdt.internal.compiler.env.NameEnvironmentAnswer;
import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory;

/**
 * EclipseCompiler -- An implementation of Compiler that uses the
 * Eclipse JDT compiler to convert Java source to a class file.
 * Because this is a pure in-memory Java compiler, it does not
 * required the a classpath to be setup, and instead relies upon a
 * class loader.
 *
 * @author Jeffrey Ichnowski
 * @version $Revision: 8 $
 */
public class EclipseCompiler extends org.owasp.jxt.compiler.Compiler {

    private static String convertVersion(String input) {
        if (null == input || "1.5".equals(input) || "5".equals(input)) {
            return CompilerOptions.VERSION_1_5;
        } else if ("1.6".equals(input)) {
            return CompilerOptions.VERSION_1_6;
        } else if ("1.4".equals(input)) {
            return CompilerOptions.VERSION_1_4;
        } else if ("1.3".equals(input)) {
            return CompilerOptions.VERSION_1_3;
        } else if ("1.2".equals(input)) {
            return CompilerOptions.VERSION_1_2;
        } else if ("1.1".equals(input)) {
            return CompilerOptions.VERSION_1_1;
        } else {
            return CompilerOptions.VERSION_1_5;
        }
    }

    @Override
    @SuppressWarnings({"deprecation", "unchecked"})
    public final CompileResults compile(String ... classNames) {
        // The bulk of the work is done in the helper inner-class
        // "CompileContext".  This method sets up the context,
        // compiler options and compiler, then calls the ".compile()"
        // method.

        CompileContext ctx = new CompileContext(classNames);

        String generateDebugInfo =
            _debug ? CompilerOptions.GENERATE : CompilerOptions.DO_NOT_GENERATE;

        Map settings = new HashMap();
        settings.put(CompilerOptions.OPTION_LineNumberAttribute,
                     CompilerOptions.GENERATE);
        settings.put(CompilerOptions.OPTION_SourceFileAttribute,
                     CompilerOptions.GENERATE);
        settings.put(CompilerOptions.OPTION_ReportDeprecation,
                     CompilerOptions.IGNORE);
        settings.put(CompilerOptions.OPTION_Encoding,
                     "UTF-8");
        settings.put(CompilerOptions.OPTION_LocalVariableAttribute,
                     generateDebugInfo);
        settings.put(CompilerOptions.OPTION_Source, convertVersion(_source));
        settings.put(CompilerOptions.OPTION_TargetPlatform, convertVersion(_target));

        IErrorHandlingPolicy errorPolicy =
            DefaultErrorHandlingPolicies.proceedWithAllProblems();

        IProblemFactory problemFactory =
            new DefaultProblemFactory(Locale.getDefault());

        Compiler compiler = new Compiler(
            ctx, errorPolicy, settings, ctx, problemFactory, true);

        compiler.compile(ctx.getCompilationUnits());

        return ctx.getResults();
    }

    /**
     * Converts a dotted package name and optional type name into a
     * single string.
     *
     * @param pkgParts the package name (e.g. "com", "sf", "Sfv4")
     * @param name the type name (e.g. "Example")
     * @return the package and type joined by dotted notation
     * (e.g. "com.sf.sfv4.Example")
     */
    static String join(char[][] pkgParts, char[] name) {
        StringBuilder buf = new StringBuilder();
        String sep = "";

        if (pkgParts != null) {
            for (char[] part : pkgParts) {
                buf.append(sep).append(part);
                sep = ".";
            }
        }

        if (name != null) {
            buf.append(sep).append(name);
        }

        return buf.toString();
    }

    /**
     * The compilation context for a single invocation to the compile
     * method.  Implements the ICompilerRequestor and INameEnvironment
     * for the compiler.  Also tracks errors, the compilation units
     * (java source files) and the compilation result.
     */
    final class CompileContext implements ICompilerRequestor, INameEnvironment {
        static final int BUFFER_SIZE = 16 * 1024;

        final StringWriter _messages = new StringWriter();
        final PrintWriter _log = new PrintWriter(_messages);

        boolean _success;
        ICompilationUnit[] _compilationUnits;

        /** Map from class name to ICompilationUnit used to look up
         * source files being compiled by this invocation. */
        Map<String,ICompilationUnit> _targetClassMap =
            new HashMap<String,ICompilationUnit>();

        /**
         * Creates the compilation context for a given set of class
         * names.  The class names are resolved to files based upon a
         * source directory (specified in the Compiler base class).
         *
         * @param classNames the classes to compile
         */
        CompileContext(String ... classNames) {
            ICompilationUnit[] cmpUnits = new ICompilationUnit[classNames.length];
            for (int i=0 ; i<classNames.length ; ++i) {
                String className = classNames[i];
                cmpUnits[i] = new CompilationUnit(this, classToSourceFile(className), className);
                _targetClassMap.put(className, cmpUnits[i]);
            }
            _compilationUnits = cmpUnits;
        }

        public ICompilationUnit[] getCompilationUnits() {
            return _compilationUnits;
        }

        public void log(String msg) {
            _log.println(msg);
        }

        public CompileResults getResults() {
            _log.flush();
            return new CompileResults(_success, _messages.toString());
        }

        public void acceptResult(CompilationResult result) {
            // System.out.println("acceptResult "+result.hasProblems());

            boolean errors = false;

            if (result.hasProblems()) {
                for (IProblem prob : result.getProblems()) {
                    if (prob.isError()) {
                        log(new StringBuilder()
                            .append(prob.getOriginatingFileName())
                            .append(':')
                            .append(prob.getSourceLineNumber())
                            .append(": ")
                            .append(prob.getMessage())
                            .toString());

                        errors = true;
                    }
                }
            }

            if (!errors) {
                try {
                    for (ClassFile classFile : result.getClassFiles()) {
                        String name = join(classFile.getCompoundName(), null);
                        File outFile = new File(
                            _destDir, name.replace('.', '/') + ".class");

                        outFile.getParentFile().mkdirs();

                        FileOutputStream fos = new FileOutputStream(outFile);
                        try {
                            fos.write(classFile.getBytes());
                        } finally {
                            fos.close();
                        }
                    }
                } catch (IOException e) {
                    log("IOException: "+e);
                }

                _success = true;
            }
        }

        public NameEnvironmentAnswer findType(char[][] compoundTypeName) {
            return findType(join(compoundTypeName, null));
        }

        public NameEnvironmentAnswer findType(char[] typeName, char[][] pkgName) {
            return findType(join(pkgName, typeName));
        }

        /**
         * Implementation of findType.  It first looks in the classes
         * that are being compiled by this invocation and returns one
         * of those if referenced.  If not, it calles upon the
         * classLoader to get a class's byte stream as a resource
         * stream.  (Because of this the EclipseCompiler does not need
         * to classpath to be specified as the JavacCompiler does.)
         */
        private NameEnvironmentAnswer findType(String className) {
            // System.out.println("findType: "+className);
            ICompilationUnit compUnit = _targetClassMap.get(className);
            if (compUnit != null) {
                return new NameEnvironmentAnswer(compUnit, null);
            }

            String rsrcName = className.replace('.', '/') + ".class";

            try {
                InputStream in = _classLoader.getResourceAsStream(rsrcName);
                if (null == in) {
                    return null;
                }
                try {
                    byte[] buf = new byte[BUFFER_SIZE];
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    for (;;) {
                        int n = in.read(buf);
                        if (n == -1) {
                            break;
                        }
                        baos.write(buf, 0, n);
                    }
                    byte[] classBytes = baos.toByteArray();
                    char[] fileName = className.toCharArray();
                    ClassFileReader classFileReader = new ClassFileReader(
                        classBytes, fileName, true);

                    return new NameEnvironmentAnswer(
                        classFileReader, null);
                } finally {
                    in.close();
                }
            } catch (IOException e) {
                log("IOException: "+e);
            } catch (ClassFormatException e) {
                log("ClassFormatException: "+e.getMessage());
            }

            return null;
        }

        public boolean isPackage(char[][] parentPackageName, char[] packageName) {
            String pkgName = join(parentPackageName, packageName);

            // System.out.println("isPackage: "+pkgName);

            if (_targetClassMap.containsKey(pkgName)) {
                return false;
            }
            String rsrcName = pkgName.replace('.', '/') + ".class";
            InputStream in = _classLoader.getResourceAsStream(rsrcName);
            if (in == null) {
                return true;
            }

            try {
                in.close();
            } catch (IOException e) {
                e = null; // ignore
            }

            return false;
        }

        public void cleanup() {
        }
    }

    /**
     * A single compilation unit representing a class name and the
     * file with the source.
     */
    static final class CompilationUnit implements ICompilationUnit {
        static final int BUFFER_SIZE = 8 * 1024;

        CompileContext _ctx;
        File _sourceFile;
        String _targetClass;

        CompilationUnit(CompileContext ctx, File sourceFile, String targetClass) {
            _ctx = ctx;
            _sourceFile = sourceFile;
            _targetClass = targetClass;
        }

        public char[] getFileName() {
            return _sourceFile.toString().toCharArray();
        }

        public char[] getContents() {
            // System.out.println("getContents: "+_targetClass);
            try {
                FileInputStream fis = new FileInputStream(_sourceFile);
                try {
                    InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
                    char[] data = new char[BUFFER_SIZE];
                    StringBuilder buf = new StringBuilder();
                    for (;;) {
                        int n = isr.read(data);
                        if (n == -1) {
                            break;
                        }
                        buf.append(data, 0, n);
                    }
                    data = new char[buf.length()];
                    buf.getChars(0, data.length, data, 0);
                    return data;
                } finally {
                    fis.close();
                }
            } catch (IOException e) {
                _ctx.log("IOException: "+e.getMessage());
                return null;
            }
        }

        public char[] getMainTypeName() {
            int i = _targetClass.lastIndexOf('.');
            return (i != -1 ? _targetClass.substring(i+1) : _targetClass).toCharArray();
        }

        public char[][] getPackageName() {
            String[] parts = _targetClass.split("\\.");
            int n = parts.length-1;
            char[][] result = new char[n][];
            for (int i=0 ; i<n ; ++i) {
                result[i] = parts[i].toCharArray();
            }
            return result;
        }
    }

} // EclipseCompiler
